#!/usr/bin/env python

#Licensed : GNU Public License 3, I believe

"""A collaborative psychohistorical web application built on Google App Engine."""

__author__ = 'Antoine Goutenoir'

import datetime
import os
import random
import string
import sys
import wsgiref.handlers
import logging
import html_sanitizer


from google.appengine.api import users
from google.appengine.ext import db
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp.util import login_required

# Set to true if we want to have our webapp print stack traces, etc
_DEBUG = True

# Add our custom Django template filters to the built in filters
template.register_template_library('templatefilters')

################################################################################

class Event(db.Model):
  """An historical or speculative event"""
  name       = db.StringProperty(required=True)
  published  = db.BooleanProperty(default=False)
  type       = db.TextProperty(default='spec')
  archived   = db.BooleanProperty(default=False)

  @staticmethod
  def get_current_user_events():
    """Returns the events that the current user has access to as admin"""
    return Event.get_user_events(users.GetCurrentUser())

  @staticmethod
  def get_user_events(user):
    """Returns the events that the given user has access to as admin"""
    if not user: return []
    memberships = db.Query(EventAdmin).filter('user =', user)
    return [m.event for m in memberships]

  @staticmethod
  def get_public_events():
    """Returns the public events"""
    published = db.Query(Event).filter('published =', True)
    return [p for p in published]

  def current_user_has_access(self):
    """Returns true if the current user has access to this event."""
    return self.user_has_access(users.GetCurrentUser())

  def user_has_access(self, user):
    """Returns true if the given user has access to this event."""
    if not user: return False
    if self.user_is_admin(user):
      return True
    else:
      return self.publish

  def current_user_is_admin(self):
    """Returns true if the current user is admin of this event."""
    return self.user_is_admin(users.GetCurrentUser())

  def user_is_admin(self, user):
    """Returns true if the given user is admin of this event."""
    if not user: return False
    query = db.Query(EventAdmin)
    query.filter('event =', self)
    query.filter('user =', user)
    return query.get()

class EventAdmin(db.Model):
  """Specifies which user is an admin on which event"""
  event = db.Reference(Event, required=True)
  user  = db.UserProperty(required=True)

class Condition(db.Model):
  """A condition for an event.
  The completed field is a DateTime, not a bool; if it is not None, the
  task is completed, and the timestamp represents the time at which it was
  marked completed."""
  event       = db.Reference(Event, required=True)
  description = db.TextProperty(required=True)
  completed   = db.DateTimeProperty()
  priority    = db.IntegerProperty(required=True, default=0)
  created     = db.DateTimeProperty(auto_now_add=True)
  updated     = db.DateTimeProperty(auto_now=True)
  user        = db.UserProperty(required=True)
  
  def can_edit(self, user):
    if not user: return False
    if self.user == user or self.event.current_user_is_admin():
      return True
    else:
      return False

  def current_user_can_edit(self):
    return self.can_edit(users.GetCurrentUser())
  
  def get_comments(self):
    """Get the comments for this condition, sorted by creation date"""
    # We could use conditioncomment_set, but how order it ? ===> .order() ! 
    query = db.Query(ConditionComment)
    query.filter('condition =', self)
    query.order('created')
    return [c for c in query]

  def get_nb_comments(self):
    """Get the number of comments for this condition"""
    return len(self.get_comments())

class ConditionComment(db.Model):
  """ A single comment on a condition """
  condition = db.Reference(Condition, required=True)
  user      = db.UserProperty(required=True)
  comment   = db.TextProperty(required=True)
  created   = db.DateTimeProperty(auto_now_add=True)
  show_vote = db.BooleanProperty(default=False) # Shows the vote if it exists

class ConditionOpinion(db.Model):
  """ The opinion of a user about a particular condition """
  condition  = db.Reference(Condition, required=True)
  user       = db.UserProperty(required=True)
  created    = db.DateTimeProperty(auto_now_add=True)
  updated    = db.DateTimeProperty(auto_now=True)
  irrelevant = db.BooleanProperty(default=False)
  checked    = db.BooleanProperty()
  comment    = db.Reference(ConditionComment) # with "Wanna explain why ?"

################################################################################
################################################################################

class BaseRequestHandler(webapp.RequestHandler):
  """Supplies a common template generation function.

  When you call generate(), we augment the template variables supplied with
  the current user in the 'user' variable and the current webapp request
  in the 'request' variable.
  """
  def generate(self, template_name, template_values={}):
    values = {
      'request': self.request,
      'user': users.GetCurrentUser(),
      'login_url': users.CreateLoginURL(self.request.uri),
      'logout_url': users.CreateLogoutURL('http://' + self.request.host + '/'),
      #'debug': self.request.get('deb'),
      'debug': True,
      'application_name': 'Black Drops of Spleen',
    }
    values.update(template_values)
    directory = os.path.dirname(__file__)
    path = os.path.join(directory, os.path.join('templates', template_name))
    self.response.out.write(template.render(path, values, debug=_DEBUG))

################################################################################
################################################################################

class IndexPage(BaseRequestHandler):
  """ Shows the main page for any (un-)logged visitor """
  def get(self):
    events = Event.get_public_events()
    past_events = [e for e in events if e.type == 'past']
    spec_events = [e for e in events if e.type == 'spec']
    
    self.generate('index.html', {
      'past_events': past_events,
      'spec_events': spec_events,
    })

class InboxPage(BaseRequestHandler):
  """Lists the events "inbox" for the current user."""
  @login_required
  def get(self):
    events = Event.get_current_user_events()
    public_events = Event.get_public_events()
    
    # Completion calculus
    for e in public_events:
      query = db.Query(Condition).filter('event =', e)
      nb_total = query.count()
      nb_checked = len([c for c in query if c.completed])
      if nb_total:
        e.completion = int(round(100 * nb_checked / nb_total))
      else:
        e.completion = 0
    
    # Total number of comments
    for e in public_events:
      e.total_comments = 0
      query_conditions = db.Query(Condition).filter('event =', e)
      for c in query_conditions:
        query_comments = db.Query(ConditionComment).filter('condition =', c)
        e.total_comments += query_comments.count()
        
    
    show_archive = self.request.get('archive')
#    if not show_archive:
#      non_archived = []
#      for task_list in lists:
#        if not task_list.archived:
#          non_archived.append(task_list)
#      lists = non_archived

    self.generate('inbox.html', {
      'events': events,
      'public_events': public_events,
      'archive': show_archive,
    })

################################################################################

class EventPage(BaseRequestHandler):
  """Displays a single event based on ID.

  If the event is not published, we give a 403 unless the user is a
  admin on the event. If it is published, but the user is not an admin
  collaborator, we show the more limited HTML view of the event rather
  than the interactive AJAXy edit page.
  """

  # The different task list output types we support: content types and
  # template file extensions
  _OUTPUT_TYPES = {
    'default': ['text/html', 'html'],
    'admin': ['text/html', 'html'],
#    'html': ['text/html', 'html'],
#    'atom': ['application/atom+xml', 'xml'],
  }

  def get(self):
    event = Event.get(self.request.get('id'))
    if not event:
      self.error(403)
      return

    # Choose a template based on the output type
    output_name = self.request.get('output')
    output_name_list = EventPage._OUTPUT_TYPES.keys()
    if output_name not in output_name_list:
      output_name = output_name_list[0]
    output_type = EventPage._OUTPUT_TYPES[output_name]

    # Validate this user has access to this event.
    if not event.current_user_has_access():
      user = users.GetCurrentUser()
      if not user:
        self.redirect(users.CreateLoginURL(self.request.uri))
      else:
        self.error(403)
      return

    # Validate if this user asks admin access
    if output_name == 'admin':
      if not event.current_user_is_admin():
        self.error(403)
#    if output_name == output_name_list[0] and task_list.current_user_is_admin():
#      output_name = 'admin'
    
#    # Filter out archived tasks by default
#    show_archive = self.request.get('archive')
#    tasks = task_list.task_set.order('-priority').order('created')
#    if not show_archive:
#      tasks.filter('archived =', False)
#    tasks = list(tasks)

    # Get the conditions for this event
    #conditions = Condition.all().filter('event', event)
    conditions = event.condition_set.order('-priority').order('created')
    conditions = list(conditions)

    # Get the last updated date from the list of conditions
    if len(conditions) > 0:
      updated = max([condition.updated for condition in conditions])
    else:
      updated = None

    # Get the number of comments
    for c in conditions:
      c.nb_comments = c.get_nb_comments()

    self.response.headers['Content-Type'] = output_type[0]
    self.generate('event_' + output_name + '.' + output_type[1], {
      'event': event,
      'conditions': conditions,
#      'archive': show_archive,
      'updated': updated,
    })

class ConditionPage(BaseRequestHandler):
  """ Shows a condition and its comments """
  def get(self):
    condition = Condition.get(self.request.get('id'))
    if not condition:
      self.error(403)
      return
    event = condition.event
    
    # Authenticate
    if not event.current_user_has_access():
        user = users.GetCurrentUser()
        if not user:
          self.redirect(users.CreateLoginURL(self.request.uri))
        else:
          self.error(403)
        return

    # Get comments
    comments = condition.get_comments()

    self.generate('condition.html', {
      'event': event,
      'condition': condition,
      'comments': comments,
    })

################################################################################

class AboutPage(BaseRequestHandler):
  """ Shows an About page """
  def get(self):
    self.generate('about.html', {
      'arg1': "argument1" # dummy
    })

################################################################################

class CreateEventAction(BaseRequestHandler):
  """Creates a new event for the current user."""
  def post(self):
    user = users.GetCurrentUser()
    name = self.request.get('name')
    if not user or not name:
      self.error(403)
      return

    event = Event(name=name)
    event.put()
    event_member = EventAdmin(event=event, user=user)
    event_member.put()

    if self.request.get('next'):
      self.redirect(self.request.get('next'))
    else:
      self.redirect('/event?id=' + str(event.key()) + '&output=admin')

class EditConditionAction(BaseRequestHandler):
  """Edits a specific condition, changing its description.

  We also updated the last modified date of the event so that the
  event inbox shows the correct last modified date.

  This can be used in an AJAX way or in a form. In a form, you should
  supply a "next" argument that denotes the URL we should redirect to
  after the edit is complete.
  """
  def post(self):
    description = self.request.get('description')
    if not description:
      self.error(403)
      return

    # Get the existing condition that we are editing
    condition_key = self.request.get('condition')
    if condition_key:
      condition = Condition.get(condition_key)
      if not condition:
        self.error(403)
        return
      event = condition.event
    else:
      condition = None
      event = Event.get(self.request.get('event'))

    # Validate this user has access to this event
    if not event or not event.current_user_has_access():
      self.error(403)
      return

    # Create the condition
    user = users.GetCurrentUser()
    if condition:
#      if not condition.user: # obsolete now
#        condition.user = user
      if not condition.current_user_can_edit():
        self.error(403)
        return
      condition.description = db.Text(description)
    else:
      condition = Condition(description=db.Text(description),
                            event=event,
                            user=user)
    condition.put()

    # Update the event so it's updated date is updated. Saving it is all
    # we need to do since that field has auto_now=True
    event.put()

    # Only redirect if "next" is given
    next = self.request.get('next')
    if next:
      self.redirect(next)
    else:
      self.response.headers['Content-Type'] = 'text/plain'
      self.response.out.write(str(condition.key()))

class AddMemberAction(BaseRequestHandler):
  """Adds a new User to an Event ACL."""
  def post(self):
    event = Event.get(self.request.get('event'))
    email = self.request.get('email')
    if not event or not email:
      self.error(403)
      return
    # Validate this user has access as admin to this event
    if not event.current_user_is_admin():
      self.error(403)
      return
    # Don't duplicate entries in the permissions datastore
    user = users.User(email)
    if not event.user_is_admin(user):
      member = EventAdmin(user=user, event=event)
      member.put()
    self.redirect(self.request.get('next'))


class SetConditionCompletedAction(BaseRequestHandler):
  """Sets a given condition to be completed at the current time."""
  def post(self):
    condition = Condition.get(self.request.get('id'))
    if not condition or not condition.current_user_can_edit():
      self.error(403)
      return

    completed = self.request.get('completed')
    if completed:
      condition.completed = datetime.datetime.now()
    else:
      condition.completed = None
      condition.archived = False
    condition.put()


class SetConditionPositionsAction(BaseRequestHandler):
  """Orders the conditions in an event.

  The input to this handler is a comma-separated list of condition keys in the
  "conditions" argument to the post. We assign priorities to the given conditions
  based on that order (e.g., 1 through N for N conditions).
  """
  def post(self):
    keys = self.request.get("conditions").split(",")
    if not keys:
      self.error(403)
      return
    num_keys = len(keys)
    for i, key in enumerate(keys):
      key = keys[i]
      condition = Condition.get(key)
      if not condition or not condition.current_user_can_edit():
        self.error(403)
        return
      condition.priority = num_keys - i - 1
      condition.put()

class EventAction(BaseRequestHandler):
  """Performs an action on a specific condition, or multiple conditions

  The actions we support are "Delete", as specified
  by the "action" argument in the POST.
  """
  def post(self):
    action = self.request.get('action')
    conditions = self.request.get('condition', allow_multiple=True)
    if not action in ['Delete']:
      self.error(403)
      return

    for key in conditions:
      condition = Condition.get(key)

      # Validate this user has access to this condition list
      if not condition or not condition.current_user_can_edit():
        self.error(403)
        return

      if action == 'Delete':
        condition.delete()
      else:
        if condition.completed and not condition.archived:
          condition.priority = 0
          condition.archived = True
          condition.put()

    self.redirect(self.request.get('next'))

class PublishEventAction(BaseRequestHandler):
  """Publishes a given event, which makes it viewable by everybody."""
  def post(self):
    event = Event.get(self.request.get('id'))
    if not event or not event.current_user_is_admin():
      self.error(403)
      return

    event.published = bool(self.request.get('publish'))
    event.put()

class ChangeTypeEventAction(BaseRequestHandler):
  """Changes the type of a given event, as past or speculative."""
  def post(self):
    event = Event.get(self.request.get('id'))
    if not event or not event.current_user_is_admin():
      self.error(403)
      return
    type = self.request.get('type')
    if not type in ['past', 'spec']:
      self.error(403)
      return
    event.type = type
    event.put()


class AddConditionCommentAction(BaseRequestHandler):
  """Adds a comment to a condition"""
  def post(self):
    condition = Condition.get(self.request.get('condition_id'))
    if not condition or not condition.current_user_can_edit():
      self.error(403)
      return
    comment = self.request.get('comment')
    comment = html_sanitizer.sanitize_html(comment)
    comment = db.Text(comment, encoding="utf-8")
    user = users.GetCurrentUser()
    condition_comment = ConditionComment(condition=condition, user=user,
                                         comment=comment)
    condition_comment.put();


################################################################################

################################################################################
################################################################################

class InboxAction(BaseRequestHandler):
  """Performs an action in the user's Event inbox.

  We support Archive, Unarchive, and Delete actions. The action is specified
  by the "action" argument in the POST. The names are capitalized because
  they correspond to the text in the buttons in the form, which all have the
  name "action".
  """
  def post(self):
    action = self.request.get('action')
    events = self.request.get('event', allow_multiple=True)
    if not action in ['Archive', 'Unarchive', 'Delete']:
      self.error(403)
      return

    for key in events:
      event = Event.get(key)

      # Validate this user has access to this task list
      if not event or not event.current_user_is_admin():
        self.error(403)
        return

      if action == 'Archive':
        event.archived = True
        event.put()
      elif action == 'Unarchive':
        event.archived = False
        event.put()
      else:
        for member in event.eventadmin_set:
          member.delete()
        for condition in event.condition_set:
          condition.delete()
        event.delete()

    self.redirect(self.request.get('next'))


################################################################################

def main():
  application = webapp.WSGIApplication([
    ('/', IndexPage),
    ('/inbox', InboxPage),
    ('/event', EventPage),
    ('/about', AboutPage),
    ('/condition', ConditionPage),
    ('/inboxaction.do', InboxAction),
    ('/event.do', EventAction),
    ('/createevent.do', CreateEventAction),
    ('/publishevent.do', PublishEventAction),
    ('/changetypeevent.do', ChangeTypeEventAction),
    ('/addmember.do', AddMemberAction),
    ('/editcondition.do', EditConditionAction),
    ('/setconditioncompleted.do', SetConditionCompletedAction),
    ('/setconditionpositions.do', SetConditionPositionsAction),
    ('/addconditioncomment.do', AddConditionCommentAction),
  ], debug=_DEBUG)
  wsgiref.handlers.CGIHandler().run(application) # Magic line !

if __name__ == '__main__':
  main() # Main is phat <3
